package total

import (
	"context"
	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/lotus/blockstore"
	lotusMiner "github.com/filecoin-project/lotus/chain/actors/builtin/miner"
	"github.com/filecoin-project/lotus/chain/gen"
	"github.com/filecoin-project/lotus/chain/store"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/milkbobo/fishgoweb/language"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"math/big"
	"miners-monitor/models/baseFee"
	"miners-monitor/models/common"
	"miners-monitor/models/miner"
	"sync"
)

type TotalAoModel struct {
	common.BaseModel
	CommonFunc common.CommonFunc
	TotalDb    TotalDbModel
	BaseFeeAo  baseFee.BaseFeeAoModel
	MinerAo    miner.MinerAoModel
}

var SyncRoundWinnerMap map[string]int64

func (this *TotalAoModel) Find(filter bson.D, opts *options.FindOptions) []TotalMiner {
	mongoResult, err := this.TotalDb.Find(filter, opts)
	result := []TotalMiner{}
	err = mongoResult.All(context.TODO(), &result)
	if err == mongo.ErrNoDocuments {
		return []TotalMiner{}
	} else if err != nil {
		panic(err)
	}
	return result
}

func (this *TotalAoModel) FindStruct(filter TotalMiner, opts *options.FindOptions) []TotalMiner {
	filterBsonByte, err := this.CommonFunc.BsonMarshal(filter)
	if err != nil {
		panic(err)
	}
	mongoResult, err := this.TotalDb.Find(filterBsonByte, opts)
	result := []TotalMiner{}
	err = mongoResult.All(context.TODO(), &result)
	if err == mongo.ErrNoDocuments {
		return []TotalMiner{}
	} else if err != nil {
		panic(err)
	}
	return result
}

func (this *TotalAoModel) FindOneStruct(filter TotalMiner, opts *options.FindOneOptions) TotalMiner {
	filterBsonByte, err := this.CommonFunc.BsonMarshal(filter)
	if err != nil {
		panic(err)
	}
	mongoResult := this.TotalDb.FindOne(filterBsonByte, opts)
	result := TotalMiner{}
	err = mongoResult.Decode(&result)
	if err == mongo.ErrNoDocuments {
		return TotalMiner{}
	} else if err != nil {
		panic(err)
	}
	return result
}

func (this *TotalAoModel) FindOne(filter bson.D, opts *options.FindOneOptions) TotalMiner {
	mongoResult := this.TotalDb.FindOne(filter, opts)
	result := TotalMiner{}
	err := mongoResult.Decode(&result)
	if err == mongo.ErrNoDocuments {
		return TotalMiner{}
	} else if err != nil {
		panic(err)
	}
	return result
}

func (this *TotalAoModel) AddTotal(targetTipSet *types.TipSet, minerId string) TotalMiner {
	ctx := context.Background()
	singleMinerAddr := this.MinerAo.GetMinerAddress()[minerId]
	insertData := TotalMiner{}
	insertData.MinerAddress = minerId
	insertData.Height = int64(targetTipSet.Height())
	insertData.Time = int64(targetTipSet.Blocks()[0].Timestamp)

	this.BaseFeeAo.Upsert(baseFee.BaseFee{
		Gas:    targetTipSet.Blocks()[0].ParentBaseFee.String(),
		Height: insertData.Height,
		Time:   insertData.Time,
	})

	mact, err := common.LotusApi.StateGetActor(ctx, singleMinerAddr, targetTipSet.Key())
	if err == nil {
		insertData.TotalBalance = mact.Balance.String()
		stor := store.ActorStore(ctx, blockstore.NewAPIBlockstore(common.LotusApi))
		mas, err := lotusMiner.Load(stor, mact)

		if err == nil {
			lockedFunds, err := mas.LockedFunds()
			if err == nil {
				insertData.InitialPledgeRequirement = lockedFunds.InitialPledgeRequirement.String()
				insertData.PreCommitDeposits = lockedFunds.PreCommitDeposits.String()
				insertData.VestingFunds = lockedFunds.VestingFunds.String()
			}
		}
	}

	pow, err := common.LotusApi.StateMinerPower(ctx, singleMinerAddr, targetTipSet.Key())
	if err == nil {
		insertData.QualityAdjPower = pow.MinerPower.QualityAdjPower.String()
		if !pow.MinerPower.QualityAdjPower.IsZero() {
			insertData.PowerRatio = float64(types.BigDiv(types.BigMul(pow.MinerPower.QualityAdjPower, types.NewInt(1000000)), pow.TotalPower.QualityAdjPower).Int64()) / 10000
		}
	}

	mi, err := common.LotusApi.StateMinerInfo(ctx, singleMinerAddr, targetTipSet.Key())
	if err == nil {

		ob, err := common.LotusApi.StateGetActor(ctx, mi.Owner, targetTipSet.Key())
		if err == nil {
			insertData.OwnerBalance = ob.Balance.String()
		}

		wb, err := common.LotusApi.StateGetActor(ctx, mi.Worker, targetTipSet.Key())
		if err == nil {
			insertData.WorkerBalance = wb.Balance.String()
		}

		controlBalance := big.NewInt(0)
		for _, ca := range mi.ControlAddresses {
			cb, err := common.LotusApi.StateGetActor(ctx, ca, targetTipSet.Key())
			if err == nil {
				insertData.ControlBalance = append(insertData.ControlBalance, cb.Balance.String())
				controlBalance = big.NewInt(0).Add(cb.Balance.Int, controlBalance)
			}
		}
	}
	secCounts, err := common.LotusApi.StateMinerSectorCount(ctx, singleMinerAddr, targetTipSet.Key())
	if err == nil {
		insertData.CommittedPower = types.BigMul(types.NewInt(secCounts.Live), types.NewInt(uint64(mi.SectorSize))).String()
		insertData.FaultySectors = int(secCounts.Faulty)
	}

	var WorkerKey address.Address
	insertData.RoundWinner, WorkerKey = this.GetRoundWinner(ctx, singleMinerAddr, targetTipSet)
	if insertData.RoundWinner != TotalRoundWinnerEnum.YES {
		this.MinerAo.AddWorkerKey(minerId, WorkerKey)
	}

	deadlines, err := common.LotusApi.StateMinerDeadlines(ctx, singleMinerAddr, targetTipSet.Key())
	if err == nil {
		for dlIdx, _ := range deadlines {
			partitions, err := common.LotusApi.StateMinerPartitions(ctx, singleMinerAddr, uint64(dlIdx), targetTipSet.Key())
			if err == nil {
				sectors := uint64(0)
				for _, partition := range partitions {
					sc, err := partition.AllSectors.Count()
					if err == nil {
						sectors += sc
					}
				}
				if sectors > 0 {
					insertData.ProvingDeadlines++
					continue
				}
			}
		}
	}

	if (insertData.QualityAdjPower == "" || insertData.QualityAdjPower == "0") && (insertData.TotalBalance == "" || insertData.TotalBalance == "0") {
		return insertData
	}
	this.TotalDb.Upsert(insertData)
	return insertData
}

func (this *TotalAoModel) AddByHeight(targetTipSet *types.TipSet) {
	minerIds := this.MinerAo.GetMinerIds()
	for _, singleMinerId := range minerIds {
		this.AddTotal(targetTipSet, singleMinerId)
	}
}

func (this *TotalAoModel) CountDocuments(filter bson.D, opts *options.CountOptions) int64 {
	return this.TotalDb.CountDocuments(filter, opts)
}

func (this *TotalAoModel) SyncRoundWinner(minerId string) {
	height := int64(644468)
	//height := int64(common.NowTipSet.Height())
	minerIds := this.MinerAo.GetMinerIds()
	if minerId != "" {
		minerIds = []string{minerId}
	}
	minerAddressMap := this.MinerAo.GetMinerAddress()
	var wg sync.WaitGroup
	var ml sync.Mutex

	for {
		//this.Log.Debug("%+v", height)
		for _, singleMinerId := range minerIds {
			wg.Add(1)
			go func(singleMinerId string) {
				defer language.CatchCrash(func(exception language.Exception) {
					this.Log.Critical("SyncVestingFunds Crash Code:[%d] Message:[%s]\nStackTrace:[%s]", exception.GetCode(), exception.GetMessage(), exception.GetStackTrace())
				})
				defer wg.Done()
				totalData := this.FindOneStruct(TotalMiner{MinerAddress: minerId, Height: height}, options.FindOne())
				if totalData.MinerAddress == "" {
					return
				}
				if totalData.RoundWinner == TotalRoundWinnerEnum.YES {
					return
				}
				singleMinerAddr := minerAddressMap[singleMinerId]
				targetTipSet, err := common.LotusApi.ChainGetTipSetByHeight(context.TODO(), abi.ChainEpoch(height), common.NowTipSet.Key())
				if err != nil {
					panic(err)
				}
				//roundWinner, WorkerKey := this.GetRoundWinner(context.TODO(), singleMinerAddr, targetTipSet)
				roundWinner, _ := this.GetRoundWinner(context.TODO(), singleMinerAddr, targetTipSet)

				//if roundWinner != TotalRoundWinnerEnum.YES &&
				//	roundWinner != TotalRoundWinnerEnum.NO {
				//	this.MinerAo.AddWorkerKey(singleMinerId, WorkerKey)
				//}
				this.TotalDb.Upsert(TotalMiner{
					Height:       int64(targetTipSet.Height()),
					MinerAddress: singleMinerId,
					RoundWinner:  roundWinner,
				})
				ml.Lock()
				SyncRoundWinnerMap[singleMinerId] = height
				ml.Unlock()
			}(singleMinerId)
		}
		wg.Wait()
		height--
		if height < 1 {
			break
		}
	}
}

var SyncVestingFundsByHeight int64

func (this *TotalAoModel) SyncVestingFunds(minerId string, height int64) {
	nowHeight := int64(common.NowTipSet.Height())

	if height != 0 {
		nowHeight = height
	}
	for {
		search := TotalMiner{Height: nowHeight, MinerAddress: minerId}
		datas := this.FindStruct(search, options.Find())
		for _, v := range datas {
			if v.VestingFunds == "" || v.VestingFunds == "0" {
				singleMinerAddr := this.MinerAo.GetMinerAddress()[v.MinerAddress]
				targetTipSet, err := common.LotusApi.ChainGetTipSetByHeight(context.TODO(), abi.ChainEpoch(nowHeight), common.NowTipSet.Key())
				if err != nil {
					panic(err)
				}
				mact, err := common.LotusApi.StateGetActor(context.TODO(), singleMinerAddr, targetTipSet.Key())
				if err == nil {
					stor := store.ActorStore(context.TODO(), blockstore.NewAPIBlockstore(common.LotusApi))
					mas, err := lotusMiner.Load(stor, mact)

					if err == nil {
						lockedFunds, err := mas.LockedFunds()
						if err == nil {
							v.VestingFunds = lockedFunds.VestingFunds.String()
							this.TotalDb.Upsert(TotalMiner{
								Height:       v.Height,
								MinerAddress: v.MinerAddress,
								VestingFunds: v.VestingFunds,
							})
						}
					}
				}
			}
		}
		//this.Log.Debug("%+v",height)
		if height != 0 {
			//this.Log.Debug("%+v",height)
			break
		}
		nowHeight--
		SyncVestingFundsByHeight = nowHeight
		if nowHeight < 1 {
			break
		}
	}
}

func (this *TotalAoModel) GetRoundWinner(ctx context.Context, addr address.Address, targetTipSet *types.TipSet) (int8, address.Address) {
	mbi, err := common.LotusApi.MinerGetBaseInfo(ctx, addr, targetTipSet.Height(), targetTipSet.Key())
	if err != nil {
		//this.Log.Debug("%+v",err)
		return 0, address.Address{}
	}
	if err == nil && mbi != nil && mbi.EligibleForMining {
		rbase := mbi.PrevBeaconEntry
		entries := mbi.BeaconEntries
		if len(entries) > 0 {
			rbase = entries[len(entries)-1]
		}

		winner, err := gen.IsRoundWinner(ctx, targetTipSet, targetTipSet.Height(), addr, rbase, mbi, common.LotusApi)
		if err != nil {
			//this.Log.Debug("IsRoundWinner.Error,%+v,%+v,%+v", addr.String(), int64(targetTipSet.Height()), err.Error())
			return TotalRoundWinnerEnum.NOT_EXIST, mbi.WorkerKey
		} else if winner == nil {
			return TotalRoundWinnerEnum.NO, mbi.WorkerKey
		} else {
			return TotalRoundWinnerEnum.YES, mbi.WorkerKey
		}
	}
	return 0, address.Address{}
}
